﻿/* 
 * Copyright 2010 Justin Pealing
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using Minnow.Web.Http;

namespace Minnow.Web
{
    /// <summary>
    /// Simple HTTP Server.
    /// </summary>
    /// <remarks>
    /// This class is not thread safe.
    /// </remarks>
    public class WebServer : IDisposable
    {
        private HttpListener listener = new HttpListener();

        /// <summary>
        /// Occurrs just after the server begins to process a request.
        /// </summary>
        public event EventHandler<ProcessingRequestEventArgs> ProcessingRequest;

        /// <summary>
        /// Occurrs whenever a http handler throws an exception, just before a response is sent back to the client.
        /// </summary>
        public event EventHandler<ExceptionEventArgs> HandlerException;

        /// <summary>
        /// Occurrs just before the server finishes processing a response.
        /// </summary>
        public event EventHandler<ResponseEventArgs> Response;

        /// <summary>
        /// Gets and sets the path to the local directory which server requests map onto.
        /// </summary>
        public string LocalRoot
        {
            get
            {
                return this.localRoot;
            }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    this.localRoot = value;
                }
                else
                {
                    this.localRoot = Path.GetFullPath(value);
                }
            }
        }
        private string localRoot;

        /// <summary>
        /// Gets and sets the root Url from which requests should be mapped.
        /// </summary>
        /// <example>
        /// If the RootUrl is "/MyRoot", and the local root is "c:\MinnowRoot", then the request url "/MyRoot/somefile.html"
        /// would map onto the local path "c:\MinnowRoot\somefile.html".
        /// </example>
        /// <remarks>
        /// This property will always return the root Url formatted with a trailing and leading '/'
        /// </remarks>
        public string RootUrl
        {
            get
            {
                return this.rootUrl;
            }
            set
            {
                // TODO: Handle attempts at modification when running
                string newUrl = value;
                if (string.IsNullOrEmpty(newUrl))
                {
                    newUrl = "/";
                }
                else 
                {
                    if (newUrl[0] != '/')
                    {
                        newUrl = '/' + newUrl;
                    }
                    if (newUrl[newUrl.Length - 1] != '/')
                    {
                        newUrl = newUrl + '/';
                    }
                }
                this.rootUrl = newUrl;
            }
        }
        private string rootUrl = "/";

        /// <summary>
        /// Gets the Uniform Resource Identifier (URI) prefixes handled by this server object.
        /// </summary>
        public HttpListenerPrefixCollection Prefixes
        {
            get
            {
                // TODO: Modification when running
                return this.listener.Prefixes;
            }
        }

        /// <summary>
        /// Gets the error handlers that will be used by this server object when responding to errors.
        /// </summary>
        public ErrorHandler ErrorHandler
        {
            get
            {
                return this.errorHandler;
            }
        }
        private ErrorHandler errorHandler = new ErrorHandler();

        /// <summary>
        /// Gets the collection of hanlder mappings for this server object.
        /// </summary>
        /// <remarks>
        /// When determining what handler to use the server will search through this collection looking for the
        /// first mapping that matches, hence the order of elements in this collection is important - the most general
        /// handler mappings should be at the end of this collection.
        /// </remarks>
        public List<HandlerMapping> HandlerMappings
        {
            get
            {
                // TODO: Thread safety, null values
                return this.handlerMappings;
            }
        }
        private List<HandlerMapping> handlerMappings = new List<HandlerMapping>();

        /// <summary>
        /// Gets the list of the file names of default documents for this server object.
        /// </summary>
        public List<string> DefaultDocuments
        {
            get
            {
                // TODO: Thread safety, null values
                return this.defaultDocuments;
            }
        }
        private List<string> defaultDocuments = new List<string>();

        /// <summary>
        /// Starts this web server object.
        /// </summary>
        public void Start()
        {
            // TODO check running state
            this.InitialiseHandlers();
            listener.Start();
            listener.BeginGetContext(new AsyncCallback(WorkerMain), listener);
        }

        /// <summary>
        /// Stops this web server object
        /// </summary>
        public void Stop()
        {
            // TODO check running state
            this.listener.Stop();
        }

        /// <summary>
        /// Gets the local path for a given request URL.
        /// </summary>
        /// <param name="RequestUri">Request Url</param>
        /// <returns>Path to the local file, or null if either RootDirectory is null or the Uri is 
        /// not rooted under the RootUri.</returns>
        public string GetLocalPath(Uri RequestUri)
        {
            if (string.IsNullOrEmpty(this.LocalRoot))
            {
                return null;
            }
            else
            {
                Uri rootUri = new Uri("http://" + RequestUri.Host + this.RootUrl);
                Uri relativeUrl = new Uri("http://" + RequestUri.Host + '/' + rootUri.MakeRelativeUri(RequestUri).ToString());
                return Path.GetFullPath(Path.Combine(this.LocalRoot, relativeUrl.LocalPath.Substring(1)));
            }
        }

        /// <summary>
        /// Initialises all of the request http handlers
        /// </summary>
        private void InitialiseHandlers()
        {
            foreach (HandlerMapping mapping in this.HandlerMappings)
            {
                mapping.Handler.Initialise(this.LocalRoot, this.RootUrl);
            }
        }

        /// <summary>
        /// Handles a HTTP request context
        /// </summary>
        /// <param name="Context">Request context to handle</param>
        private void HandleContext(HttpListenerContext Context)
        {
            IHttpRequest request = new ListenerHttpRequest(Context.Request);
            this.OnProcessingRequest(request);

            string filePath = this.GetLocalPath(request.Url);
            HandlerMapping mapping = this.GetHandler(ref filePath);

            ListenerHttpResponse response = new ListenerHttpResponse(Context.Response);
            if (mapping != null)
            {
                try
                {
                    mapping.Handler.Process(request, response, filePath);
                }
                catch (Exception ex)
                {
                    this.OnHandlerException(ex, mapping.Handler);
                    this.ErrorHandler.HandleError(response, HttpErrorStatus.Status500);
                }
            }
            else
            {
                this.ErrorHandler.HandleError(response, HttpErrorStatus.Status404);
            }

            this.OnResponse(request, response, mapping);
            Context.Response.Close();
        }

        /// <summary>
        /// Finds the HttpHanlder that should be used to process a request.
        /// </summary>
        /// <param name="LocalPath">The complete local path that the request Url maps on to, or null if either the server has no local document root or the request url
        /// is not rooted within the servers root url.
        /// If a suitable default document is used then the local path parameter will be updated to account for the default document.</param>
        /// <returns>IHttpHandler to use, or null if no suitable handler could be found.</returns>
        private HandlerMapping GetHandler(ref string LocalPath)
        {
            if (LocalPath == null)
            {
                return this.SearchForHandler(string.Empty, LocalMappingState.None);
            }
            else
            {
                // If the local path maps onto a directory then we should check for default documents first
                if (Directory.Exists(LocalPath))
                {
                    for (int i = 0; i < this.DefaultDocuments.Count; i++)
                    {
                        string document = this.DefaultDocuments[i];
                        string fullPath = Path.Combine(LocalPath, document);
                        HandlerMapping returnValue = this.SearchForHandler(document, File.Exists(fullPath) ? LocalMappingState.File : LocalMappingState.None);
                        if (returnValue != null)
                        {
                            LocalPath = fullPath;
                            return returnValue;
                        }
                    }

                    return this.SearchForHandler(string.Empty, LocalMappingState.Folder);
                }
                else
                {
                    return this.SearchForHandler(Path.GetFileName(LocalPath), File.Exists(LocalPath) ? LocalMappingState.File : LocalMappingState.None);
                }
            }
        }

        /// <summary>
        /// Searches through all mappings for one that matches the given request.
        /// </summary>
        /// <param name="FileName">Name of the local file that the request Url maps onto.</param>
        /// <param name="Exists">True if the file exists, flase otherwise</param>
        /// <returns>The first matching handler mapping, or null if no mapping meets the given criteria.</returns>
        private HandlerMapping SearchForHandler(string FileName, LocalMappingState LocalMapping)
        {
            for (int i = 0; i < this.HandlerMappings.Count; i++)
            {
                HandlerMapping mapping = this.HandlerMappings[i];
                if (mapping.Matches(FileName) && mapping.Matches(LocalMapping) )
                {
                    return mapping;
                }
            }
            return null;
        }

        private void OnProcessingRequest(IHttpRequest Request)
        {
            EventHandler<ProcessingRequestEventArgs> handler = this.ProcessingRequest;
            if (handler != null)
            {
                handler(this, new ProcessingRequestEventArgs(Request));
            }
        }

        private void OnHandlerException(Exception Exception, RequestHandler Handler)
        {
            EventHandler<ExceptionEventArgs> handler = this.HandlerException;
            if (handler != null)
            {
                handler(this, new ExceptionEventArgs(Exception, Handler));
            }
        }

        private void OnResponse(IHttpRequest Request, IHttpResponse Response, HandlerMapping Handler)
        {
            EventHandler<ResponseEventArgs> handler = this.Response;
            if (handler != null)
            {
                handler(this, new ResponseEventArgs(Request, Response, Handler));
            }
        }

        /// <summary>
        /// Main background method
        /// </summary>
        /// <param name="result"></param>
        private void WorkerMain(IAsyncResult result)
        {
            HttpListener listener = (HttpListener)result.AsyncState;

            HttpListenerContext context;
            try
            {
                context = listener.EndGetContext(result);
            }
            catch (HttpListenerException ex)
            {
                if (ex.ErrorCode == 995)
                {
                    return;
                }
                throw;
            }

            listener.BeginGetContext(new AsyncCallback(WorkerMain), listener);
            this.HandleContext(context);
        }

        #region IDisposable Members

        public void Dispose()
        {
            // TODO: this
            if (this.listener.IsListening)
            {
                this.listener.Stop();
            }
        }

        #endregion
    }
}